/*
 * File    : Timer.java
 * Created : 21-Nov-2003
 * By      : parg
 * 
 * Azureus - a Java Bittorrent client
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details ( see the LICENSE file ).
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 */

package org.gudy.azureus2.core3.util;

/**
 * @author parg
 *
 */

import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import java.util.TreeSet;

public class Timer extends AERunnable implements SystemTime.ChangeListener {
    private static boolean DEBUG_TIMERS = true;
    private static ArrayList<WeakReference<Timer>> timers = null;
    private static AEMonitor timers_mon = new AEMonitor("timers list");

    private ThreadPool thread_pool;

    private Set<TimerEvent> events = new TreeSet<TimerEvent>();

    private long unique_id_next = 0;

    private long current_when;
    private volatile boolean destroyed;
    private boolean indestructable;

    private boolean log;
    private int max_events_logged;

    public Timer(String name) {
        this(name, 1);
    }

    public Timer(String name, int thread_pool_size) {
        this(name, thread_pool_size, Thread.NORM_PRIORITY);
    }

    public Timer(String name, int thread_pool_size, int thread_priority) {
        if (DEBUG_TIMERS) {
            try {
                timers_mon.enter();
                if (timers == null) {
                    timers = new ArrayList<WeakReference<Timer>>();
                    AEDiagnostics.addEvidenceGenerator(new evidenceGenerator());
                }
                timers.add(new WeakReference<Timer>(this));
            } finally {
                timers_mon.exit();
            }
        }

        thread_pool = new ThreadPool(name, thread_pool_size);

        SystemTime.registerClockChangeListener(this);

        Thread t = new Thread(this, "Timer:" + name);

        t.setDaemon(true);

        t.setPriority(thread_priority);

        t.start();
    }

    public void setIndestructable() {
        indestructable = true;
    }

    public synchronized List<TimerEvent> getEvents() {
        return (new ArrayList<TimerEvent>(events));
    }

    public void setLogging(boolean _log) {
        log = _log;
    }

    public boolean getLogging() {
        return log;
    }

    public void setWarnWhenFull() {
        thread_pool.setWarnWhenFull();
    }

    public void setLogCPU() {
        thread_pool.setLogCPU();
    }

    public void runSupport() {
        while (true) {

            try {
                TimerEvent event_to_run = null;

                synchronized (this) {

                    if (destroyed) {

                        break;
                    }

                    if (events.isEmpty()) {

                        // System.out.println( "waiting forever" );

                        try {
                            current_when = Integer.MAX_VALUE;

                            this.wait();

                        } finally {

                            current_when = 0;
                        }
                    } else {

                        long now = SystemTime.getCurrentTime();

                        TimerEvent next_event = (TimerEvent) events.iterator().next();

                        long when = next_event.getWhen();

                        long delay = when - now;

                        if (delay > 0) {

                            // System.out.println( "waiting for " + delay );

                            try {
                                current_when = when;

                                this.wait(delay);

                            } finally {

                                current_when = 0;
                            }
                        }
                    }

                    if (destroyed) {

                        break;
                    }

                    if (events.isEmpty()) {

                        continue;
                    }

                    long now = SystemTime.getCurrentTime();

                    Iterator<TimerEvent> it = events.iterator();

                    TimerEvent next_event = it.next();

                    long rem = next_event.getWhen() - now;

                    if (rem <= SystemTime.TIME_GRANULARITY_MILLIS) {

                        event_to_run = next_event;

                        it.remove();

                        /*
                         * if ( rem < -100 ){ System.out.println( "Late scheduling [" + (-rem) + "] of " + event_to_run.getString()); }
                         */
                    }

                    // System.out.println( getName() +": events=" + events.size() + ", to_run=" +
                    // (event_to_run==null?"null":event_to_run.getString()));
                }

                if (event_to_run != null) {

                    event_to_run.setHasRun();

                    if (log) {
                        System.out.println("running: " + event_to_run.getString());
                    }

                    thread_pool.run(event_to_run.getRunnable());
                }

            } catch (Throwable e) {

                Debug.printStackTrace(e);
            }
        }
    }

    public void clockChangeDetected(long current_time, long offset) {
        if (Math.abs(offset) >= 60 * 1000) {

            // fix up the timers

            synchronized (this) {

                Iterator<TimerEvent> it = events.iterator();

                List<TimerEvent> updated_events = new ArrayList<TimerEvent>(events.size());

                while (it.hasNext()) {

                    TimerEvent event = (TimerEvent) it.next();

                    // absolute events don't have their timings fiddled with

                    if (!event.isAbsolute()) {

                        long old_when = event.getWhen();
                        long new_when = old_when + offset;

                        TimerEventPerformer performer = event.getPerformer();

                        // sanity check for periodic events

                        if (performer instanceof TimerEventPeriodic) {

                            TimerEventPeriodic periodic_event = (TimerEventPeriodic) performer;

                            long freq = periodic_event.getFrequency();

                            if (new_when > current_time + freq + 5000) {

                                long adjusted_when = current_time + freq;

                                // Debug.outNoStack( periodic_event.getName() + ": clock change sanity check. Reduced schedule time from " + old_when
                                // + "/" + new_when + " to " + adjusted_when );

                                new_when = adjusted_when;
                            }
                        }

                        // don't wrap around by accident although this really shouldn't happen

                        if (old_when > 0 && new_when < 0 && offset > 0) {

                            // Debug.out( "Ignoring wrap around for " + event.getName());

                        } else {

                            // System.out.println( "    adjusted: " + old_when + " -> " + new_when );

                            event.setWhen(new_when);
                        }
                    }

                    updated_events.add(event);
                }

                // resort - we have to use an alternative list of events as input because if we just throw the
                // treeset in the constructor optimises things under the assumption that the original set
                // was correctly sorted...

                events = new TreeSet<TimerEvent>(updated_events);
            }
        }
    }

    public void clockChangeCompleted(long current_time, long offset) {
        if (Math.abs(offset) >= 60 * 1000) {

            // there's a chance that between the change being notified and completed an event was scheduled
            // using an un-modified current time. Nothing can be done for non-periodic events but for periodic
            // ones we can santitize them to at least be within the periodic time period of the current time
            // important for when clock goes back but not forward obviously

            synchronized (this) {

                Iterator<TimerEvent> it = events.iterator();

                boolean updated = false;

                while (it.hasNext()) {

                    TimerEvent event = (TimerEvent) it.next();

                    // absolute events don't have their timings fiddled with

                    if (!event.isAbsolute()) {

                        TimerEventPerformer performer = event.getPerformer();

                        // sanity check for periodic events

                        if (performer instanceof TimerEventPeriodic) {

                            TimerEventPeriodic periodic_event = (TimerEventPeriodic) performer;

                            long freq = periodic_event.getFrequency();

                            long old_when = event.getWhen();

                            if (old_when > current_time + freq + 5000) {

                                long adjusted_when = current_time + freq;

                                // Debug.outNoStack( periodic_event.getName() + ": clock change sanity check. Reduced schedule time from " + old_when
                                // + " to " + adjusted_when );

                                event.setWhen(adjusted_when);

                                updated = true;
                            }
                        }
                    }
                }

                if (updated) {

                    events = new TreeSet<TimerEvent>(new ArrayList<TimerEvent>(events));
                }

                // must have this notify here as the scheduling code uses the current time to calculate
                // how long to sleep for and this needs to be guaranteed to be using the correct (new) time

                notify();
            }
        }
    }

    public void adjustAllBy(long offset) {
        // fix up the timers

        synchronized (this) {

            // as we're adjusting all events by the same amount the ordering remains valid

            Iterator<TimerEvent> it = events.iterator();

            boolean resort = false;

            while (it.hasNext()) {

                TimerEvent event = it.next();

                long old_when = event.getWhen();
                long new_when = old_when + offset;

                // don't wrap around by accident

                if (old_when > 0 && new_when < 0 && offset > 0) {

                    // Debug.out( "Ignoring wrap around for " + event.getName());

                    resort = true;

                } else {

                    // System.out.println( "    adjusted: " + old_when + " -> " + new_when );

                    event.setWhen(new_when);
                }
            }

            if (resort) {

                events = new TreeSet<TimerEvent>(new ArrayList<TimerEvent>(events));
            }

            notify();
        }
    }

    public synchronized TimerEvent addEvent(long when, TimerEventPerformer performer) {
        return (addEvent(SystemTime.getCurrentTime(), when, performer));
    }

    public synchronized TimerEvent addEvent(String name, long when, TimerEventPerformer performer) {
        return (addEvent(name, SystemTime.getCurrentTime(), when, performer));
    }

    public synchronized TimerEvent addEvent(String name, long when, boolean absolute, TimerEventPerformer performer) {
        return (addEvent(name, SystemTime.getCurrentTime(), when, absolute, performer));
    }

    public synchronized TimerEvent addEvent(long creation_time, long when, TimerEventPerformer performer) {
        return (addEvent(null, creation_time, when, performer));
    }

    public synchronized TimerEvent addEvent(long creation_time, long when, boolean absolute, TimerEventPerformer performer) {
        return (addEvent(null, creation_time, when, absolute, performer));
    }

    public synchronized TimerEvent addEvent(String name, long creation_time, long when, TimerEventPerformer performer) {
        return (addEvent(name, creation_time, when, false, performer));
    }

    public synchronized TimerEvent addEvent(String name, long creation_time, long when, boolean absolute, TimerEventPerformer performer) {
        TimerEvent event = new TimerEvent(this, unique_id_next++, creation_time, when, absolute, performer);

        if (name != null) {

            event.setName(name);
        }

        events.add(event);

        if (log) {

            if (events.size() > max_events_logged) {

                max_events_logged = events.size();

                System.out.println("Timer '" + thread_pool.getName() + "' - events = " + max_events_logged);
            }
        }

        // System.out.println( "event added (" + when + ") - queue = " + events.size());

        if (current_when == Integer.MAX_VALUE || when < current_when) {

            notify();
        }

        return (event);
    }

    public synchronized TimerEventPeriodic addPeriodicEvent(long frequency, TimerEventPerformer performer) {
        return (addPeriodicEvent(null, frequency, performer));
    }

    public synchronized TimerEventPeriodic addPeriodicEvent(String name, long frequency, TimerEventPerformer performer) {
        return (addPeriodicEvent(name, frequency, false, performer));
    }

    public synchronized TimerEventPeriodic addPeriodicEvent(String name, long frequency, boolean absolute, TimerEventPerformer performer) {
        TimerEventPeriodic periodic_performer = new TimerEventPeriodic(this, frequency, absolute, performer);

        if (name != null) {

            periodic_performer.setName(name);
        }

        if (log) {

            System.out.println("Timer '" + thread_pool.getName() + "' - added " + periodic_performer.getString());
        }

        return (periodic_performer);
    }

    protected synchronized void cancelEvent(TimerEvent event) {
        if (events.contains(event)) {

            events.remove(event);

            // System.out.println( "event cancelled (" + event.getWhen() + ") - queue = " + events.size());

            notify();
        }
    }

    public synchronized void destroy() {
        if (indestructable) {

            Debug.out("Attempt to destroy indestructable timer '" + getName() + "'");

        } else {

            destroyed = true;

            notify();

            SystemTime.unregisterClockChangeListener(this);
        }

        if (DEBUG_TIMERS) {
            try {
                timers_mon.enter();
                // crappy
                for (Iterator iter = timers.iterator(); iter.hasNext();) {
                    WeakReference timerRef = (WeakReference) iter.next();
                    Object timer = timerRef.get();
                    if (timer == null || timer == this) {
                        iter.remove();
                    }
                }
            } finally {
                timers_mon.exit();
            }
        }
    }

    public String getName() {
        return (thread_pool.getName());
    }

    public synchronized void dump() {
        System.out.println("Timer '" + thread_pool.getName() + "': dump");

        Iterator it = events.iterator();

        while (it.hasNext()) {

            TimerEvent ev = (TimerEvent) it.next();

            System.out.println("\t" + ev.getString());
        }
    }

    private class evidenceGenerator implements AEDiagnosticsEvidenceGenerator {
        public void generate(IndentWriter writer) {
            if (!DEBUG_TIMERS) {
                return;
            }

            ArrayList lines = new ArrayList();
            int count = 0;
            try {
                try {
                    timers_mon.enter();
                    // crappy
                    for (Iterator iter = timers.iterator(); iter.hasNext();) {
                        WeakReference timerRef = (WeakReference) iter.next();
                        Timer timer = (Timer) timerRef.get();
                        if (timer == null) {
                            iter.remove();
                        } else {
                            count++;

                            List events = timer.getEvents();

                            lines.add(timer.thread_pool.getName() + ", " + events.size() + " events:");

                            Iterator it = events.iterator();
                            while (it.hasNext()) {
                                TimerEvent ev = (TimerEvent) it.next();

                                lines.add("  " + ev.getString());
                            }
                        }
                    }
                } finally {
                    timers_mon.exit();
                }

                writer.println("Timers: " + count + " (time=" + SystemTime.getCurrentTime() + "/" + SystemTime.getMonotonousTime() + ")");
                writer.indent();
                for (Iterator iter = lines.iterator(); iter.hasNext();) {
                    String line = (String) iter.next();
                    writer.println(line);
                }
                writer.exdent();
            } catch (Throwable e) {
                writer.println(e.toString());
            }
        }
    }
}
